(ns instant.superadmin.routes
  (:require [clojure.walk :as w]
            [compojure.core :as compojure :refer [defroutes DELETE GET POST]]
            [instant.db.model.attr :as attr-model]
            [instant.model.app :as app-model]
            [instant.model.instant-personal-access-token :as instant-personal-access-token-model]
            [instant.model.instant-user :as instant-user-model]
            [instant.model.member-invites :as member-invites-model]
            [instant.model.oauth-app :as oauth-app-model]
            [instant.model.org :as org-model]
            [instant.model.rule :as rule-model]
            [instant.model.schema :as schema-model]
            [instant.postmark :as postmark]
            [instant.util.email :as email]
            [instant.util.exception :as ex]
            [instant.util.http :as http-util]
            [instant.util.roles :refer [assert-least-privilege!
                                        get-app-with-role!]]
            [instant.util.string :as string-util]
            [instant.util.token :as token-util]
            [instant.util.uuid :as uuid-util]
            [ring.util.http-response :as response])
  (:import
   (java.util UUID)))

(defn get-org-with-role! [{:keys [user org-id role]}]
  (let [org-with-role (org-model/get-org-for-user! {:org-id org-id
                                                    :user-id (:id user)})]
    (assert-least-privilege! role (:role org-with-role))
    {:org org-with-role}))

(defn req->superadmin-user!
  "We're reusing some of the superadmin routes for the OAuth app platform routes.
   The scope is only applicable to requests that authenticate with a
   token generated by the OAuth app"
  [scope req]
  (let [token (http-util/req->bearer-token! req)]
    (if (token-util/is-platform-access-token? token)
      (let [access-token-record
            (oauth-app-model/access-token-by-token-value!
             {:access-token (token-util/platform-access-token-value token)})
            scope-str (case scope
                        :apps/read oauth-app-model/apps-read-scope
                        :apps/write oauth-app-model/apps-write-scope
                        :data/read oauth-app-model/data-read-scope
                        :data/write oauth-app-model/data-write-scope
                        :storage/read oauth-app-model/storage-read-scope
                        :storage/write oauth-app-model/storage-write-scope
                        ;; We don't let the OAuth app transfer apps,
                        ;; using an unused scope to prevent it.
                        :apps/transfer "apps-transfer")]
        (if-not (oauth-app-model/satisfies-scope? (:scopes access-token-record)
                                                  scope-str)
          (ex/throw-missing-scope! scope-str)
          (instant-user-model/get-by-id! {:id (:user_id access-token-record)})))
      (let [personal-access-token (if (token-util/is-personal-access-token? token)
                                    token
                                    ;; Backwards compatibility for tokens generated before 5/16/2025
                                    (token-util/->PersonalAccessToken (str token)))]
        (instant-user-model/get-by-personal-access-token!
         {:personal-access-token personal-access-token})))))

(defn req->superadmin-user-and-app! [scope role req]
  (let [user (req->superadmin-user! scope req)
        app-id (ex/get-param! req [:params :app_id] uuid-util/coerce)]
    (get-app-with-role! {:user user
                         :app-id app-id
                         :role role})))

(defn req->superadmin-app! [scope role req]
  (let [token (http-util/req->bearer-token! req)]
    (if (or (token-util/is-platform-access-token? token)
            (token-util/is-personal-access-token? token)
            (instant-user-model/get-by-personal-access-token
             {:personal-access-token
              (token-util/->PersonalAccessToken (str token))}))
      (:app (req->superadmin-user-and-app! scope role req))

      (if-let [app (app-model/get-by-admin-token {:token token})]
        app
        (ex/throw+ {::ex/type ::ex/record-not-found
                    ::ex/message "Record not found: token"})))))

(defn enhance-apps
  "Adds schema and perms to apps if the request asked for them."
  [req body-with-apps]
  (let [includes (some-> (ex/get-optional-param! req
                                                 [:params :include]
                                                 string-util/coerce-non-blank-str)
                         (String/.split ",")
                         set)]
    (cond-> body-with-apps
      (contains? includes "schema")
      (update :apps
              (fn [apps]
                (let [attrs-by-app (attr-model/get-by-app-ids (map :id apps))]
                  (map (fn [app]
                         (assoc app :schema (schema-model/attrs->schema
                                             (get attrs-by-app (:id app)))))
                       apps))))

      (contains? includes "perms")
      (update :apps
              (fn [apps]
                (let [rules-by-app (rule-model/get-by-app-ids
                                    {:app-ids (map :id apps)})]
                  (map (fn [app]
                         (assoc app :perms (get-in rules-by-app
                                                   [(:id app) :code])))
                       apps)))))))

;; --------
;; Org crud

(defn orgs-list-get [req]
  (let [{user-id :id} (req->superadmin-user! :apps/read req)
        orgs (org-model/get-all-for-user {:user-id user-id})]
    (response/ok {:orgs orgs})))

(defn orgs-list-apps-get [req]
  (let [{user-id :id} (req->superadmin-user! :apps/read req)
        org-id-param (ex/get-param! req [:params :org_id] uuid-util/coerce)
        org (org-model/get-org-for-user! {:org-id org-id-param
                                          :user-id user-id})
        apps (org-model/apps-for-org {:org-id (:id org) :user-id user-id})]
    (response/ok (enhance-apps req {:apps apps}))))

;; --------
;; App crud

(defn apps-list-get [req]
  (let [{user-id :id} (req->superadmin-user! :apps/read req)
        apps (app-model/list-by-creator-id user-id)]
    (response/ok (enhance-apps req {:apps apps}))))

(defn apps-create-post [req]
  (let [{user-id :id :as user} (req->superadmin-user! :apps/write req)
        title (ex/get-param! req [:body :title] string-util/coerce-non-blank-str)
        org-id-param (ex/get-optional-param! req [:body :org_id] uuid-util/coerce)
        {:keys [org]} (when org-id-param
                        (get-org-with-role! {:user user
                                             :org-id org-id-param
                                             :role :admin}))
        schema (get-in req [:body :schema])
        rules-code (ex/get-optional-param! req [:body :perms] w/stringify-keys)
        _ (when rules-code
            (ex/assert-valid! :perms rules-code (rule-model/validation-errors
                                                 rules-code)))
        app (app-model/create! (merge {:id (random-uuid)
                                       :title title
                                       :admin-token (random-uuid)}
                                      (if org
                                        {:org-id (:id org)}
                                        {:creator-id user-id})))
        perms (when rules-code
                (rule-model/put! {:app-id (:id app)
                                  :code rules-code}))]

    (when schema
      (->> schema
           (schema-model/plan! {:app-id (:id app)
                                :check-types? true
                                :background-updates? false})
           (schema-model/apply-plan! (:id app))))

    (response/ok {:app (assoc app
                              :perms (:code perms)
                              :schema (schema-model/attrs->schema
                                       (attr-model/get-by-app-id (:id app))))})))

(defn app-details-get [req]
  (let [app (req->superadmin-app! :apps/read :collaborator req)]
    (response/ok {:app app})))

(defn app-update-post [req]
  (let [{app-id :id} (req->superadmin-app! :apps/write :admin req)
        title (ex/get-param! req [:body :title] string-util/coerce-non-blank-str)
        app (app-model/rename-by-id! {:id app-id :title title})]
    (response/ok {:app app})))

(defn app-delete [req]
  (let [{:keys [app user]} (req->superadmin-user-and-app! :apps/write :admin req)
        _ (when (and (:creator_id app)
                     (not= (:creator_id app)
                           (:id user)))
            ;; Require owner to delete a personal app, but
            ;; just admin to delete an org app
            (ex/assert-permitted! :allowed-member-role? :owner false))
        app (app-model/mark-for-deletion! {:id (:id app)})]
    (response/ok {:app app})))

;; ---------
;; Transfers

(defn transfer-app-invite-email [inviter-user app invitee-email]
  (let [title "Instant"]
    {:from (str title " <teams@pm.instantdb.com>")
     :to invitee-email
     :subject (str "[Instant] You've been asked to take ownership of " (:title app))
     :html
     (postmark/standard-body
      "<p><strong>Hey there!</strong></p>
       <p>
         " (:email inviter-user) " invited you to become the new owner of " (:title app) ".
       </p>
       <p>
         Navigate to <a href=\"https://instantdb.com/dash?s=invites\">Instant</a> to accept the invite.
       </p>
       <p>
         Note: This invite will expire in 3 days. If you
         don't know the user inviting you, please reply to this email.
       </p>")}))

(defn app-transfer-send-invite-post [req]
  (let [{:keys [app user]} (req->superadmin-user-and-app! :apps/transfer :owner req)
        invitee-email (ex/get-param! req [:body :dest_email] email/coerce)
        {app-id :id} app
        {user-id :id} user
        {invite-id :id} (member-invites-model/create!
                         {:type :app
                          :foreign-key app-id
                          :inviter-id user-id
                          :email invitee-email
                          :role "creator"})]
    (postmark/send!
     (transfer-app-invite-email user app invitee-email))
    (response/ok {:id invite-id})))

(defn app-transfer-revoke-post [req]
  (let [{{user-id :id} :user {app-id :id} :app} (req->superadmin-user-and-app! :apps/transfer :owner req)
        dest-email (ex/get-param! req [:body :dest_email] email/coerce)
        rejected-count (count (member-invites-model/reject-by-email-and-role
                               {:inviter-id user-id
                                :type :app
                                :foreign-key app-id
                                :invitee-email dest-email
                                :role "creator"}))]

    (response/ok {:count rejected-count})))

;; -----
;; Rules

(defn app-rules-get [req]
  (let [{app-id :id} (req->superadmin-app! :apps/read :collaborator req)
        {:keys [code]} (rule-model/get-by-app-id {:app-id app-id})]
    (response/ok {:perms code})))

(defn app-rules-post [req]
  (let [{app-id :id} (req->superadmin-app! :apps/write :collaborator req)
        code (ex/get-param! req [:body :code] w/stringify-keys)]
    (ex/assert-valid! :rule code (rule-model/validation-errors code))
    (response/ok {:rules (rule-model/put! {:app-id app-id
                                           :code code})})))

;; ------
;; Schema

(defn app-schema-get [req]
  (let [{app-id :id} (req->superadmin-app! :apps/read :collaborator req)
        attrs (attr-model/get-by-app-id app-id)
        schema (schema-model/attrs->schema attrs)]
    (response/ok {:schema schema})))

(defn app-schema-plan-post [req]
  (let [{app-id :id} (req->superadmin-app! :apps/read :collaborator req)
        client-defs (-> req :body :schema)
        check-types? (-> req :body :check_types)
        background-updates? (-> req :body :supports_background_updates)]
    (response/ok (schema-model/plan! {:app-id app-id
                                      :check-types? check-types?
                                      :background-updates? background-updates?}
                                     client-defs))))

(defn app-schema-apply-post [req]
  (let [{app-id :id} (req->superadmin-app! :apps/write :collaborator req)
        client-defs (-> req :body :schema)
        check-types? (-> req :body :check_types)
        background-updates? (-> req :body :supports_background_updates)
        plan (schema-model/plan! {:app-id app-id
                                  :check-types? check-types?
                                  :background-updates? background-updates?}
                                 client-defs)
        plan-result (schema-model/apply-plan! app-id plan)]
    (response/ok (merge plan plan-result))))

(comment
  (def user (instant-user-model/get-by-email {:email "stepan.p@gmail.com"}))
  (def token (instant-personal-access-token-model/create! {:id (UUID/randomUUID)
                                                           :user-id (:id user)
                                                           :name "Test Token"}))
  (def headers {"authorization" (str "Bearer " (:token token))})
  (def app-response (apps-create-post {:headers headers :body {:title "Demo App"}}))
  (def app-id (-> app-response :body :app :id))

  (apps-list-get {:headers headers
                  :params {:include "schema,perms"}})
  (app-details-get {:headers headers :params {:app_id app-id}})
  (app-update-post {:headers headers :params {:app_id app-id} :body {:title "Updated Demo App"}})
  (app-transfer-send-invite-post {:headers headers
                                  :params {:app_id app-id}
                                  :body {:dest_email "stopa@instantdb.com"}})
  (app-transfer-revoke-post {:headers headers
                             :params {:app_id app-id}
                             :body {:dest_email "stopa@instantdb.com"}})
  (app-delete {:headers headers :params {:app_id app-id}}))

(defroutes routes
  (GET "/superadmin/apps" [] apps-list-get)
  (GET "/superadmin/orgs" [] orgs-list-get)
  (GET "/superadmin/orgs/:org_id/apps" [] orgs-list-apps-get)
  (POST "/superadmin/apps" [] apps-create-post)
  (GET "/superadmin/apps/:app_id" [] app-details-get)
  (POST "/superadmin/apps/:app_id" [] app-update-post)

  (POST "/superadmin/apps/:app_id/transfers/send" [] app-transfer-send-invite-post)
  (POST "/superadmin/apps/:app_id/transfers/revoke" [] app-transfer-revoke-post)

  (GET "/superadmin/apps/:app_id/schema" [] app-schema-get)
  (POST "/superadmin/apps/:app_id/schema/push/plan" [] app-schema-plan-post)
  (POST "/superadmin/apps/:app_id/schema/push/apply" [] app-schema-apply-post)

  (GET "/superadmin/apps/:app_id/perms" [] app-rules-get)
  (POST "/superadmin/apps/:app_id/perms" [] app-rules-post)

  (DELETE "/superadmin/apps/:app_id" [] app-delete))
